package it.softphone.cmc.server.rhino;

import it.softphone.cmc.server.service.ScriptletManagerService;
import it.softphone.cmc.shared.entity.ScriptletAsset;
import it.softphone.commons.service.LogAware;
import it.softphone.commons.service.scripting.ScriptingService;
import it.softphone.commons.spring.ApplicationConfig;

import java.io.IOException;
import java.io.Reader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.apache.commons.configuration.Configuration;

/**
 * Default scripting service implementation. it use a default <a href="https://scripting.dev.java.net/">javascript engine embed in JDK6</a>
 * 
 * <br/>
 * 
 * on istantation it load the function outlined below
 * 
 * <br/>
 * <pre>
 * function toMap( o ) // convert js object in java map
 * function toList( o ) // convert js array in a java list 
 * function mapToObject( map ) // convert java map to java object
 * function listToObject( list ) // convert java list to js array
 * </pre>
 * 
 * @author softphone
 *
 */
public class RequireJScriptingServiceImpl implements ScriptingService, LogAware {

	public static final String COMMON_SCRIPT_CONFIG = "common";

	private final ScriptEngine engine;
	
	public class Evaluator {
		private Pattern urlPattern = Pattern.compile("\\./([\\w\\d]+)\\.js");
		
		public void evaluate( String name ) {

			Matcher m = urlPattern.matcher(name);
			
			if( m.matches() ) {
				
				final ScriptletManagerService service = ApplicationConfig.getBean(ScriptletManagerService.class);
				
				final String key = m.group(1);
				final ScriptletAsset scriptlet = service.findScriptletByKey( key );
			
				logger.info( String.format("evaluate('%s') as key [%s]", name, key));
			
				try {
					
					RequireJScriptingServiceImpl.this.eval( scriptlet.getContent() );
				} catch (ScriptException e) {
					logger.error("evaluator error!", e);
				}	
			
			}	
		}
	}
	
	/**
	 * The configuration entry should be:<br/>
	 * <pre>
	 * script.engine (default 'JavaScript')
	 * script.common (default 'scripting/common.js')
	 * script.main (default 'scripting/main.js')
	 * script.development (default '${script.main}')
	 * 
	 * </pre>
	 * 
	 * @param scriptConf configuration instance
	 * @throws ScriptException
	 */
	public RequireJScriptingServiceImpl(Configuration scriptConf) throws  ScriptException {
		ScriptEngineManager manager = new ScriptEngineManager();
		
		if( logger.isDebugEnabled() ) {
			StringBuilder msg = new StringBuilder();
			
			java.util.List<ScriptEngineFactory> factories = manager.getEngineFactories();
			
			msg.append( String.format("ScriptEngineFactory supported # [%d]", factories.size() )).append('\n');
			
			for( ScriptEngineFactory f : factories ) {
			
				msg.append('\t')	
					.append( 
						String.format("ScriptEngineFactory name=[%s] version=[%s] languageName = [%s]",
								f.getEngineName(), 
								f.getEngineVersion(),
								f.getLanguageName()
								))
					.append('\n');

				java.util.List<String> names = f.getNames();
				if( names!=null ) {
					msg.append("\t\t").append("shortName: ");
					for( String name : names ) {
						msg.append("[").append(name).append("] ");						
					}
					msg.append('\n');
				}
				java.util.List<String> extensions = f.getExtensions();
				if( extensions!=null ) {
					msg.append("\t\t").append("extension: ");
					for( String extension : extensions ) {
						msg.append("[").append(extension).append("] ");						
					}
					msg.append('\n');
				}
				java.util.List<String> mimeTypes = f.getMimeTypes();
				if( mimeTypes!=null ) {
					msg.append("\t\t").append("mimeType: ");
					for( String mimeType : mimeTypes ) {
						msg.append("[").append(mimeType).append("] ");						
					}
					msg.append('\n');
				}		
			}
			
			logger.debug( msg.toString() );
		}
		
		final String script_engine = scriptConf.getString("engine", "JavaScript");
		
		logger.info( String.format("SCRIPTING ENGINE TAKEN [%s]", script_engine) );
		
		engine = manager.getEngineByName(script_engine);
		if( null==engine ) throw new IllegalStateException( "impossible initialize script engine!");
		
		final String common_script = scriptConf.getString(COMMON_SCRIPT_CONFIG, "scripting/commons.js");
		
		loadScripts(common_script);
		
		{
			java.io.InputStream is = getClass().getClassLoader().getResourceAsStream("scripting/r.js");
			if( is!=null )	{
				engine.put("arguments", new Object[0]); // RequireJS fix
				
				engine.put("evaluator", new Evaluator());
				engine.eval( new StringBuilder() 
							.append("function load(url) { print(url); evaluator.evaluate(url); }")					
							.toString()
						);
			

				engine.eval( new java.io.InputStreamReader( is ) );
			}
			else {
				logger.warn( String.format( "script resource [%s] not found!", "scripting/r.js"));				
			}
		}
	}

	/**
	 * 
	 * @param scriptResource
	 * @throws IOException 
	 * @throws ScriptException 
	 * @throws Exception
	 */
	private void loadScripts( String scriptResource )  {
		
		java.util.Enumeration<java.net.URL> resources = null;;
		try {
			resources = getClass().getClassLoader().getResources(scriptResource);
		} catch (IOException e1) {
			logger.warn( String.format( "error loading resources [%s]", scriptResource), e1);				
		}
		
		while ( resources.hasMoreElements() ) {
			
			java.net.URL resourceURL = resources.nextElement();

			java.io.InputStream is;
			try {
				is = resourceURL.openStream();
				if( is!=null )	{
					engine.eval( new java.io.InputStreamReader( is ) );
				}
				else {
					logger.warn( String.format( "script resource [%s] not found!", scriptResource));			
				}
			} catch (IOException e) {
				logger.warn( String.format( "error loading resource [%s]", resourceURL), e);				
			}
			catch( ScriptException ex ) {
				logger.warn( String.format( "error evaluating resource [%s]", resourceURL), ex);				
				
			}
			
		}
	}
	
	/**
	 * 
	 * @param method
	 * @param args
	 * @return
	 */
	public Object invokeFunctionSafe( String method, Object... args )  {
		
		try {
			
			Invocable inv = (Invocable) engine;

			return inv.invokeFunction(method, (Object[])args);
					
		} catch (Exception e) {
			
			logger.warn( String.format( "invocation of method [%s] has failed!", method), e);
		}
		
		return null;
	}
	
	/**
	 * 
	 * @param method
	 * @param args
	 * @return
	 * @throws NoSuchMethodException 
	 * @throws ScriptException 
	 */
	public Object invokeFunction( String method, Object... args ) throws ScriptException, NoSuchMethodException  {
		
		Object result = null;

		Invocable inv = (Invocable) engine;

		result = inv.invokeFunction(method, (Object[])args);
					
		return result;
	}


	public Bindings createBindings() {
		return engine.createBindings();
	}


	public Object eval(Reader reader, Bindings n) throws ScriptException {
		return engine.eval(reader, n);
	}


	public Object eval(Reader reader, ScriptContext context)
			throws ScriptException {
		return engine.eval(reader, context);
	}


	public Object eval(Reader reader) throws ScriptException {
		return engine.eval(reader);
	}


	public Object eval(String script, Bindings n) throws ScriptException {
		return engine.eval(script, n);
	}


	public Object eval(String script, ScriptContext context)
			throws ScriptException {
		return engine.eval(script, context);
	}


	public Object eval(String script) throws ScriptException {
		return engine.eval(script);
	}


	public Object get(String key) {
		return engine.get(key);
	}


	public Bindings getBindings(int scope) {
		return engine.getBindings(scope);
	}


	public ScriptContext getContext() {
		return engine.getContext();
	}


	public ScriptEngineFactory getFactory() {
		return engine.getFactory();
	}


	public void put(String key, Object value) {
		engine.put(key, value);
	}


	public void setBindings(Bindings bindings, int scope) {
		engine.setBindings(bindings, scope);
	}


	public void setContext(ScriptContext context) {
		engine.setContext(context);
	}


}
